#!/usr/bin/env python

# (c) 2007-2011 Helsinki University of Technology and University of Helsinki
# Licensed under the MIT license; see LICENSE.txt for more information.

import rdflib
import lrucache
import sys
import time
import urllib
import urllib2
import json

from rdf import *

CACHE_SIZE = 500

ONKIBASE = "http://www.yso.fi"
ONKITIMEOUT = 10	# seconds
ONKIMAXHITS = 1000

class LookupScheme:
  def __init__(self, scheme, uri, defaultLanguage):
    self._scheme = scheme
    self._uri = uri
    self._defaultLanguage = defaultLanguage
    
  def getURI(self):
    return self._uri
  
  def getDefaultLanguage(self):
    return self._defaultLanguage

  def _fmt(self, label, language):
    if language is not None:
      return "%s:%s@%s" % (self._scheme, label, language)
    else:
      return "%s:%s" % (self._scheme, label)
  
class OnkiLookupScheme (LookupScheme):
  def __init__(self, scheme, uri, defaultLanguage, onkiname, typerestriction, onkikey):
    LookupScheme.__init__(self, scheme, uri, defaultLanguage)
    self._onkiname = onkiname
    self._typerestriction = typerestriction
    self._onkikey = onkikey
    self._cache = lrucache.LRUCache(CACHE_SIZE)
    self._cacheHits = 0
    self._cacheMisses = 0
    self._onki = None

  def lookup(self, obj, origlabel, language=None, warn=True):
    origlabel = origlabel.strip()
    label = origlabel.lower()

    if not language:
      language = self._defaultLanguage

    cachekey = "%s@%s" % (label, language)
    if cachekey in self._cache:
      self._cacheHits += 1
      ret, isPrefMatch = self._cache[cachekey]
      if not isPrefMatch:
        obj.log('NOTE', 'map', 'Concept matched using altLabel', self._fmt(origlabel, language) )
      if warn and len(ret) == 0:
        obj.log('WARN', 'map', 'Unknown concept label', self._fmt(origlabel, language))
      if len(ret) > 1:
        obj.log('WARN', 'map', 'Ambiguous concept label', "%s (%d matches)" % (self._fmt(origlabel, language), len(ret)) )
      return ret
    self._cacheMisses += 1

    attempt = 1
    results = None

    while True:
      try:
        qs = "q=%s&l=%s&h=%d&s=0" % (urllib.quote(label.encode('UTF-8')),
                                     urllib.quote(language),
                                     ONKIMAXHITS)
        if self._typerestriction:
          qs += "&qt=" + urllib.quote(self._typerestriction)
        url = "%s/key-%s/onki/%s/rest/search?%s" % (ONKIBASE, self._onkikey, self._onkiname, qs)
        res = urllib2.urlopen(url, None, ONKITIMEOUT)
      except (urllib2.URLError, urllib2.HTTPError) as e:
        print >>sys.stderr, "Error accessing Onki (HTTP access for URL %s failed: %s). Trying again. Attempt: %d" % (url, str(e), attempt)
        attempt += 1
        time.sleep(2)

        if attempt > 5:
          print >>sys.stderr, "Accessing Onki failed. Giving up."
          raise

        continue

      try:
        results = json.load(res)['results']
        break
      except ValueError:
        print >>sys.stderr, "Error accessing Onki (JSON parsing failed). Trying again. Attempt:", attempt
        attempt += 1
        time.sleep(2)

        if attempt > 5:
          print >>sys.stderr, "Accessing Onki failed. Giving up."
          raise

    # filter away non-exact matches (however, case differences are ignored)
    results = [r for r in results if (r['label'].lower() == label or ('altLabel' in r and r['altLabel'].lower() == label))]

    prefLabelResults = [r for r in results if r['label'].lower() == label and 'altLabel' not in r]
    isPrefMatch = True
    if prefLabelResults:
      # there were prefLabel hits, so altLabel hits can be ignored
      results = prefLabelResults
    else:
      if results: # no prefLabel hits, but there were some altLabel hits
        obj.log('NOTE', 'map', 'Concept matched using altLabel', self._fmt(origlabel, language) )
        isPrefMatch = False

    if warn and len(results) == 0:
      obj.log('WARN', 'map', 'Unknown concept label', self._fmt(origlabel, language))
    
    if len(results) > 1:
      obj.log('WARN', 'map', 'Ambiguous concept label', "%s (%d matches)" % (self._fmt(origlabel, language), len(results)) )
    
    ret = [rdflib.URIRef(r['uri']) for r in results]
    self._cache[cachekey] = (ret, isPrefMatch)
    return ret

  def printStatistics(self):
    if self._cacheHits + self._cacheMisses > 0:
      hitrate = 100.0 * self._cacheHits / (self._cacheHits + self._cacheMisses)
    else:
      hitrate = -1
    print "Lookup scheme %s: cache size %d, hit rate %.1f percent" % (self._scheme, len(self._cache), hitrate)

class ModelLookupScheme (LookupScheme):
  def __init__(self, scheme, uri, defaultLanguage, classes, property, altproperty, model):
    LookupScheme.__init__(self, scheme, uri, defaultLanguage)
    self._model = model
    self._classes = classes
    self._property = property
    self._altproperty = altproperty
    
    self._map = {}

    for cl in classes:
      for obj in model.subjects(RDF.type, URIRef(cl)):
        for label in model.objects(obj, URIRef(property)):
          self.addLabel(label, obj, True)
        if altproperty:
          for label in model.objects(obj, URIRef(altproperty)):
            self.addLabel(label, obj, False)
        
  def addLabel(self, label, uri, isPrimary):
    lab = label.strip().lower() # just the value without language tag
    lang = label.language # just the language tag
    self._map.setdefault(lab, {})
    if lang is not None:
      lang = lang.lower()
      self._map[lab].setdefault(lang, [])
      self._map[lab][lang].append((lang, isPrimary, uri))
    self._map[lab].setdefault(None, [])
    self._map[lab][None].append((lang, isPrimary, uri))
    
  def lookup(self, obj, origlabel, language=None, warn=True):
    origlabel = origlabel.strip()
    label = origlabel.lower()
    
    if not language:
      language = self._defaultLanguage

    if not self._map.has_key(label):
      if warn:
        obj.log('WARN', 'map', 'Unknown concept label', self._fmt(origlabel, language))
      return []

    # match using primary property
    uris = []
    for lang,vals in self._map[label].items():
      for val in vals:
        lang, isPrimary, uri = val
        if language is not None and lang is not None and language != lang:
          continue
        if isPrimary and uri not in uris:
          uris.append(uri)
    if len(uris) > 0:
      if len(uris) > 1:
        obj.log('WARN', 'map', 'Ambiguous concept label', "%s (%d matches)" % (self._fmt(origlabel, language), len(uris)) )
      return uris

    # match using alternate property
    for lang,vals in self._map[label].items():
      for val in vals:
        lang, isPrimary, uri = val
        if language is not None and lang is not None and language != lang:
          continue
        if uri not in uris:
          uris.append(uri)

    obj.log('NOTE', 'map', 'Concept matched using altLabel', self._fmt(origlabel, language) )

    if len(uris) > 1:
      obj.log('WARN', 'map', 'Ambiguous concept label', "%s (%d matches)" % (self._fmt(origlabel, language), len(uris)) )
    
    return uris

  def printStatistics(self):
    print "Lookup scheme %s: map length %d" % (self._scheme, len(self._map))


class LookupService:
  def __init__(self, config, model):
    self._config = config
    self._model = model
    self._schemes = {}
    for vocab in self._config.getVocabularies():
      name = vocab.getName()
      uri = vocab.getURI()
      if vocab.getType() == 'onki':
        scheme = OnkiLookupScheme(name, uri, vocab.getLanguage(), vocab.getOnkiName(), vocab.getTypeRestriction(), config.getOnkiKey())
      elif vocab.getType() == 'model':
        scheme = ModelLookupScheme(name, uri, vocab.getLanguage(), vocab.getClasses(), vocab.getProperty(), vocab.getAltProperty(),
                                   model.getModel(vocab.getModelName()))
        # the model is no longer needed so it may be thrown away to save memory
        model.forgetModel(vocab.getModelName())
      else:
        print >>sys.stderr, "Warning: unknown type %s for vocabulary %s" % (vocab.getType(), name)
        continue
      # allow scheme lookup either by name, by URI or by altname
      self._schemes[name] = (scheme, 'name')
      self._schemes[uri] = (scheme, 'URI')
      for altname in vocab.getAltNames():
        self._schemes[altname] = (scheme, 'altname')
  
  def lookup(self, obj, scheme, label, language=None, warn=True):
    if scheme not in self._schemes:
      obj.log('WARN', 'map', 'Unknown scheme', scheme)
      return []
    return self._schemes[scheme][0].lookup(obj, label, language, warn)

  def getSchemeURI(self, scheme):
    if scheme not in self._schemes:
      return (None, None)
    scheme, matchtype = self._schemes[scheme]
    return (scheme.getURI(), matchtype)

  def getSchemeLanguage(self, scheme):
    if scheme not in self._schemes:
      return (None, None)
    scheme, matchtype = self._schemes[scheme]
    return (scheme.getDefaultLanguage(), matchtype)

  def printStatistics(self):
    for scheme, matchtype in self._schemes.values():
      if matchtype == 'name':
        scheme.printStatistics()
